package service

import (
	"context"
	"database/sql"
	"encoding/json"
	"fmt"
	"github.com/pkg/errors"
	"gorm.io/gorm"
	"mall/api/inventory"
	"mall/api/mall"
	"mall/api/pay"
	"mall/api/promotion"
	"mall/internal/constant"
	"mall/internal/generator"
	"mall/internal/model"
	"mall/pkg/ecode"
	"mall/pkg/orm"
	"mall/pkg/util"
	"strconv"
	"strings"
	"time"
)

func (s *Service) verifyPackages(ctx context.Context, req *mall.OrderCombineCreateRequest) (
	totalTrans uint64, packages []*inventory.Package,
	detailListMap map[int][]*model.OrderDetail, freezeDetails []*inventory.CreateFreezeOrderDetail, err error) {
	packages = make([]*inventory.Package, len(req.Orders))
	detailListMap = make(map[int][]*model.OrderDetail)
	for i, order := range req.Orders {
		var (
			shelfProds             []*model.ShelfProd
			shelfSkus              []*model.ShelfSku
			prodTotal, weightTotal uint64
		)
		prodIdMap := make(util.Int64BoolMap)
		skuIdMap := make(util.Int64BoolMap)
		for _, detail := range order.Details {
			prodIdMap[detail.ProdId] = true
			skuIdMap[detail.SkuId] = true
		}
		if shelfSkus, err = s.dao.FindOnShelfSkusBySkuIdsAndStoreIds(
			ctx, req.Token.SgId, []uint32{order.StoreId}, skuIdMap.GetKeys()); err != nil {
			return
		}
		if len(shelfSkus) != len(order.Details) {
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "当前结算商品包含已下架商品，请重新进入结算页再试")
			return
		}
		if shelfProds, err = s.dao.FindOnShelfProdsByProdIds(
			ctx, req.Token.SgId, order.StoreId, prodIdMap.GetKeys()); err != nil {
			return
		}
		shelfProdMap := make(map[uint64]*model.ShelfProd)
		shelfSkuMap := make(map[uint64]*model.ShelfSku)
		for _, shelfProd := range shelfProds {
			shelfProdMap[shelfProd.ProdId] = shelfProd
		}
		for _, shelfSku := range shelfSkus {
			shelfSkuMap[shelfSku.SkuId] = shelfSku
		}
		detailListMap[i] = make([]*model.OrderDetail, len(order.Details))
		for j, detail := range order.Details {
			shelfProd := shelfProdMap[detail.ProdId]
			shelfSku := shelfSkuMap[detail.SkuId]
			price := getCustomPrice(shelfSku)
			if detail.Origin != price {
				err = ecode.AppendMessage(
					constant.ErrForbiddenAction, "商品%s(%s)价格已经变更，请重新获取购物车提交结算页",
					getCustomName(shelfProd), strings.Join(shelfSku.SyncItems, "/"))
				return
			}
			detailTotal := detail.Origin * detail.Quantity
			prodTotal += detailTotal
			weightTotal += shelfSku.SyncWeight * detail.Quantity
			prodName := getCustomName(shelfProd)
			skuName := strings.Join(shelfSku.SyncItems, "/")
			detailListMap[i][j] = &model.OrderDetail{
				MemberId:    req.Token.MemberId,
				ProdId:      detail.ProdId,
				SkuId:       detail.SkuId,
				ProdName:    prodName,
				SkuName:     skuName,
				Pic:         shelfProd.SyncPic,
				Unit:        shelfProd.SyncUnit,
				Price:       price,
				OriginPrice: shelfSku.SyncPrice,
				Quantity:    detail.Quantity,
				Weight:      shelfSku.SyncWeight,
				Total:       detailTotal,
				Discount:    0,
				Trans:       detailTotal,
			}
			freezeDetails = append(freezeDetails, &inventory.CreateFreezeOrderDetail{
				Id:          order.WarehouseId,
				StorageType: inventory.StorageType_PROD,
				StorageKey:  fmt.Sprintf("%d-%d", detail.ProdId, detail.SkuId),
				Quantity:    detail.Quantity,
				StorageName: prodName,
				SkuName:     skuName,
			})
		}
		packages[i] = &inventory.Package{
			Id:          order.WarehouseId,
			DeliverType: order.DeliverType,
			Weight:      weightTotal,
			TotalAmount: prodTotal,
			ExpectFee:   order.DeliverFee,
		}
		if prodTotal+order.DeliverFee-order.Discount != order.Trans {
			err = ecode.BadRequest("前端%d号订单计算错误，商品总额%d，配送费%d，促销优惠-%d, 订单应付%d",
				i, prodTotal, order.DeliverFee, order.Discount, order.Trans)
			return
		}
		totalTrans += order.Trans
	}
	return
}

func (s *Service) doPromotion(ctx context.Context, req *mall.OrderCombineCreateRequest,
	promotionOrders []*promotion.TransactOrderEntity, orderDiscountMap map[string]*model.Order) (err error) {
	if len(promotionOrders) != 0 {
		var promotionResp *promotion.LockTransactionRes
		if promotionResp, err = s.downstream.SubmitPromotion(ctx, req.Token.SgId, req.Token.MemberId,
			req.RequestId, promotionOrders); err != nil {
			return
		}
		if len(promotionResp.Orders) != len(orderDiscountMap) {
			err = ecode.AppendMessage(constant.ErrRemoteError, "促销调用失败：促销返回订单数与提交订单数不一致")
			return
		}

		for _, promotionOrder := range promotionResp.Orders {
			if orderDiscountMap[promotionOrder.OutOrderNo].Discount != uint64(promotionOrder.Off) {
				err = ecode.AppendMessage(constant.ErrNeedRefresh,
					"后台订单促销金额计算不一致，请刷新订单结算页重新提交: input=%d, calculate=%d",
					orderDiscountMap[promotionOrder.OutOrderNo].Discount, promotionOrder.Off)
				return
			}
			for _, promotionItem := range promotionOrder.OfferItems {
				var promotionRemark json.RawMessage
				if promotionRemark, err = json.Marshal(&model.DiscountRemark{
					OfferType:     promotionItem.OfferType,
					OfferItemType: promotionItem.OfferItemType,
					OfferItemId:   promotionItem.OfferItemId,
					Desc:          promotionItem.OfferItemInfo.Desc,
				}); err != nil {
					return
				}
				if err = s.dao.Create(ctx, &model.OrderDiscount{
					OrderId:  orderDiscountMap[promotionOrder.OutOrderNo].ID,
					Name:     util.GetStringWithinLength(promotionItem.OfferItemInfo.Name, 32),
					Discount: uint64(promotionItem.Off),
					Type:     uint8(mall.DiscountType_DISCOUNT_TYPE_PROMOTION),
					Remark:   orm.JSON(promotionRemark),
				}); err != nil {
					return
				}
			}
		}
	}
	return
}

func (s *Service) closeBizRequestInTransaction(ctx context.Context,
	request *model.OrderRequest, now time.Time) (err error) {
	request.PayStatus = uint8(pay.PayOrderStatus_PAY_ORDER_STATUS_CLOSED)
	if err = s.dao.Save(ctx, request); err != nil {
		return
	}
	var cancelMsg json.RawMessage
	if cancelMsg, err = json.Marshal(&mall.MallBizCancelMessage{
		BizNo:     request.BizNo,
		Timestamp: now.Unix(),
	}); err != nil {
		return
	}
	if err = s.dao.PublishUniqueNSQMessage(ctx, constant.TopicBizCancel, cancelMsg); err != nil {
		return
	}
	// 删除定时任务
	if err = s.dao.DeleteSchedule(ctx,
		uint8(mall.ScheduleType_SCHEDULE_TYPE_CLOSE), request.ID); err != nil {
		return
	}
	return
}

func (s *Service) cancelBizRequestBeforeCreation(ctx context.Context,
	bizNo string, schedule *model.Schedule, now time.Time) (err error) {
	// 仅限商城订单创建前调用，业务失败 & 参数错误时，触发回滚冻结单状态 & 关闭定时任务
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		request := &model.OrderRequest{}
		if err = s.dao.LockRequest(ctx, request, bizNo); err != nil {
			return
		}
		// 仅限商城订单创建前调用，这里无需判定状态，可以直接转换
		if err = s.closeBizRequestInTransaction(ctx, request, now); err != nil {
			return
		}
		return
	}); err != nil {
		schedule.Retry += 1
		schedule.ExecuteTime = now.Add(time.Minute * time.Duration(schedule.Retry^2))
		if err = s.dao.Save(ctx, schedule); err != nil {
			return
		}
		return
	}
	return
}

// 组合下单
func (s *Service) CombineOrderCreate(ctx context.Context, req *mall.OrderCombineCreateRequest) (
	resp *mall.DefaultResp, err error) {
	var (
		totalTrans             uint64
		payRemark, addressInfo json.RawMessage
		packages               []*inventory.Package
		detailListMap          map[int][]*model.OrderDetail
		freezeDetails          []*inventory.CreateFreezeOrderDetail
		orderNumberList        []string
		sourceType             pay.SourceType
		warehouseMap           map[uint32]string
		payTypeMap             map[pay.PayType]string
	)
	now := time.Now()

	// 1. 会员订单锁，不允许同一个会员并发组合下单
	lockKey := fmt.Sprintf("mall/order/lock/%d", req.Token.MemberId)
	if !s.dao.RedisClient.SetNX(lockKey, 1, time.Second*10).Val() {
		err = ecode.AppendMessage(constant.ErrForbiddenAction,
			"您的操作速度太快，请耐心等待下单结果，不要重复点击下单按钮")
		return
	} else {
		defer s.dao.RedisClient.Del(lockKey)
	}

	// 2. 商品金额验算 / 构建明细列表
	if sourceType, err = getSourceType(req.Token); err != nil {
		return
	}
	if req.AddressInfo != nil {
		if addressInfo, err = json.Marshal(req.AddressInfo); err != nil {
			err = ecode.WithStack(err)
			return
		}
	} else {
		addressInfo = constant.EmptyBytes
	}
	if totalTrans, packages, detailListMap, freezeDetails, err = s.verifyPackages(
		ctx, req); err != nil {
		return
	}

	// 3 计费验算
	if warehouseMap, err = s.downstream.CalculateDeliverFee(ctx, &inventory.CalculateDeliverFeeRequest{
		RequestId: req.RequestId,
		Lat:       req.AddressInfo.Lat,
		Lng:       req.AddressInfo.Lng,
		CityCode:  req.AddressInfo.CityCode,
		Packages:  packages,
		Source:    inventory.SourceEnum_MALL_ORDER,
		SgId:      req.Token.SgId,
	}); err != nil {
		return
	}

	// 4. 获取可用支付方式
	if payTypeMap, err = s.downstream.GetAvailablePayTypes(ctx, req.Token.SgId, req.Orders[0].StoreId, sourceType); err != nil {
		return
	}
	payType := getPayTypeBySourceType(sourceType, req.BalancePay)
	if _, ok := payTypeMap[payType]; !ok {
		err = ecode.AppendMessage(constant.ErrForbiddenAction, "商家未配置支付收款账户")
		return
	}
	// 5. 创建请求单
	if payRemark, err = json.Marshal(&model.PayRemark{
		PayType:     payType,
		PayTypeName: payTypeMap[payType],
	}); err != nil {
		return
	}

	schedule := &model.Schedule{
		ScheduleType: uint8(mall.ScheduleType_SCHEDULE_TYPE_CLOSE),
		ExecuteTime:  now.Add(time.Minute * 30),
	}
	request := &model.OrderRequest{
		BizNo:          req.RequestId,
		SgId:           req.Token.SgId,
		PStoreId:       req.Orders[0].StoreId,
		MemberId:       req.Token.MemberId,
		SubmitTime:     now,
		Trans:          totalTrans,
		CardInfo:       orm.JSON(req.CardInfo),
		AddressInfo:    orm.JSON(addressInfo),
		PayStatus:      uint8(pay.PayOrderStatus_PAY_ORDER_STATUS_INIT),
		PayRemark:      orm.JSON(payRemark),
		AutoCancelTime: sql.NullTime{Valid: true, Time: schedule.ExecuteTime},
	}
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		// 创建请求
		if err = s.dao.Create(ctx, &request); err != nil {
			if errors.Cause(err) == constant.ErrDuplicateInput {
				err = ecode.AppendMessage(constant.ErrForbiddenAction, "请求已经存在，请查看订单列表或重新提交购物车")
				return
			}
			return
		}
		// 创建定时任务
		schedule.OrderId = request.ID
		if err = s.dao.Create(ctx, schedule); err != nil {
			return
		}
		return
	}); err != nil {
		return
	}

	// 6. 建立失败后处理任务
	defer func(errAddr *error) {
		// 异常时同样要关单
		needCancel := *errAddr != nil
		if r := recover(); r != nil {
			needCancel = true
		}
		if needCancel {
			_ = s.cancelBizRequestBeforeCreation(ctx, req.RequestId, schedule, now)
		}
	}(&err)

	// 7. 创建冻结单
	if request.FreezeOrderNumber, err = s.downstream.CreateFreezeOrder(
		ctx, req.Token.SgId, req.RequestId, freezeDetails, strconv.Itoa(int(req.Token.MemberId))); err != nil {
		return
	}
	// 8. 逐个创建商城订单(需要开启事务)
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var promotionOrders []*promotion.TransactOrderEntity
		if orderNumberList, err = generator.GenerateOrderNumberList(now, int(mall.OrderType_ORDER_TYPE_MALL),
			strconv.Itoa(int(req.Token.MemberId)), len(req.Orders)); err != nil {
			return
		}
		orderDiscountMap := make(map[string]*model.Order)
		for i, reqOrder := range req.Orders {
			var inventoryRemark json.RawMessage
			if inventoryRemark, err = json.Marshal(&model.InventoryRemark{
				WarehouseName: warehouseMap[reqOrder.WarehouseId]}); err != nil {
				return
			}
			order := &model.Order{
				OrderNumber:     orderNumberList[i],
				RequestId:       request.ID,
				SgId:            req.Token.SgId,
				PStoreId:        reqOrder.StoreId,
				MemberId:        req.Token.MemberId,
				SubmitTime:      now,
				Remark:          reqOrder.Remark,
				Source:          uint8(sourceType),
				ProdTotal:       reqOrder.Trans + reqOrder.Discount - reqOrder.DeliverFee,
				DeliverFee:      reqOrder.DeliverFee,
				Discount:        reqOrder.Discount,
				Trans:           reqOrder.Trans,
				Status:          uint8(mall.MallOrderStatus_MALL_INIT),
				PromotionStatus: reqOrder.Discount == 0,
				WarehouseId:     reqOrder.WarehouseId,
				DeliverType:     uint8(reqOrder.DeliverType),
				BookingDate:     sql.NullTime{},
				InventoryRemark: orm.JSON(inventoryRemark),
				CompleteTime:    sql.NullTime{},
				OperationRemark: constant.EmptyBytes,
				AutoConfirmTime: sql.NullTime{},
				RefundDeadline:  sql.NullTime{},
			}
			if reqOrder.BookingDays != 0 {
				order.BookingDate = sql.NullTime{
					Valid: true,
					Time: time.Date(now.Year(), now.Month(), now.Day()+int(reqOrder.BookingDays),
						0, 0, 0, 0, util.FixedLocation),
				}
			}
			if err = s.dao.Create(ctx, order); err != nil {
				return
			}
			promotionDetails := make([]*promotion.TransactOrderItemEntity, len(detailListMap[i]))
			for j, detail := range detailListMap[i] {
				detail.OrderId = order.ID
				if err = s.dao.Create(ctx, detail); err != nil {
					return
				}
				if reqOrder.Discount != 0 {
					promotionDetails[j] = &promotion.TransactOrderItemEntity{
						DetailId:   int64(detail.ID),
						DetailCnt:  int32(detail.Quantity),
						ItemId:     int64(detail.ProdId),
						ItemSpecId: int64(detail.SkuId),
						ItemPrice:  int32(detail.Price),
					}
				}
			}
			if reqOrder.Discount != 0 {
				promotionOrders = append(promotionOrders, &promotion.TransactOrderEntity{
					OwnerId:    int64(order.PStoreId),
					OutOrderNo: order.OrderNumber,
					Items:      promotionDetails,
				})
				orderDiscountMap[order.OrderNumber] = order
			}
		}
		// 9. 在事务内创建营销单，如果营销单创建失败，事务回滚，不保存订单记录，确保c端不可见
		if err = s.doPromotion(ctx, req, promotionOrders, orderDiscountMap); err != nil {
			return
		}
		request.OrderCreated = true
		if err = s.dao.Save(ctx, request); err != nil {
			return
		}
		return
	}); err != nil {
		return
	}
	resp = &mall.DefaultResp{Status: true}
	return
}

func getCustomerStatusAndDesc(order *model.Order, payStatus pay.PayOrderStatus) (
	status mall.MallCustomerStatus, statusDesc string) {
	switch mall.MallOrderStatus(order.Status) {
	case mall.MallOrderStatus_MALL_INIT:
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_WAITING_PAY
		statusDesc = "待付款"
		return
	case mall.MallOrderStatus_MALL_CLOSED_BEFORE_PAYED, mall.MallOrderStatus_MALL_CLOSED_AFTER_PAYED:
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_CLOSED
		statusDesc = "已关闭"
		return
	}
	if payStatus == pay.PayOrderStatus_PAY_ORDER_STATUS_PAYING {
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_WAITING_PAY
		statusDesc = "付款中"
		return
	}
	switch inventory.ProcessOrderStatus(order.InventoryStatus) {
	case inventory.ProcessOrderStatus_ProcessOrderWaitStock:
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_WAITING_STORAGE
		statusDesc = "待备货"
		return
	case inventory.ProcessOrderStatus_ProcessOrderWaitMentioned:
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_WAITING_PICK
		statusDesc = "待提货"
		return
	case inventory.ProcessOrderStatus_ProcessOrderWaitShip:
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_WAITING_DELIVER
		statusDesc = "待配送"
		return
	case inventory.ProcessOrderStatus_ProcessOrderShipped:
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_WAITING_SIGN
		statusDesc = "待签收"
		return
	case inventory.ProcessOrderStatus_ProcessOrderReceived:
		status = mall.MallCustomerStatus_MALL_CUSTOMER_STATUS_FINISHED
		statusDesc = "已完成"
		return
	}
	panic(fmt.Sprintf("商城订单状态无法判断: %d", order.InventoryStatus))
}

func getCustomerRefundStatusAndDesc(refund *model.Refund) (
	status mall.MallCustomerRefundStatus, statusDesc string) {
	switch mall.MallRefundStatus(refund.Status) {
	case mall.MallRefundStatus_MALL_REFUND_INIT:
		status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_APPLYING
		statusDesc = "退款申请中"
		return
	case mall.MallRefundStatus_MALL_REFUND_RETURNING:
		status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_RETURNING
		statusDesc = "退货品已寄出，等待商家签收"
		return
	case mall.MallRefundStatus_MALL_REFUND_REJECTED:
		status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_REJECTED
		statusDesc = "商家拒绝退货"
		return
	case mall.MallRefundStatus_MALL_REFUND_CLOSED:
		status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_CLOSED
		statusDesc = "退款关闭"
		return
	case mall.MallRefundStatus_MALL_REFUND_TO_BE_DELIVER:
		status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_TO_BE_RETURNED
		statusDesc = "商家同意退货，请寄回商品"
		return
	case mall.MallRefundStatus_MALL_REFUND_FINISHED:
		switch mall.MallRefundType(refund.ApplyType) {
		case mall.MallRefundType_MALL_REFUND_ONLY, mall.MallRefundType_MALL_REFUND_CANCEL_BEFORE_SENDING:
			switch pay.PayOrderStatus(refund.PayStatus) {
			case pay.PayOrderStatus_REFUND_ORDER_STATUS_MARKED_SUCCESS, pay.PayOrderStatus_REFUND_ORDER_STATUS_SUCCESS:
				status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_SUCCESS
				statusDesc = "退款成功"
				return
			case pay.PayOrderStatus_PAY_ORDER_STATUS_FAILED:
				status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_FAILED
				statusDesc = "受支付方式限制无法原路退回，请与商家及时协商线下或线上退款"
				return
			default:
				// 退款提交等状态对C端视为等待揽收
				status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_APPLYING
				statusDesc = "退款申请中"
				return
			}
		default:
			switch inventory.ProcessOrderStatus(refund.InventoryStatus) {
			case inventory.ProcessOrderStatus_ProcessOrderRejectReturn:
				status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_REJECTED
				statusDesc = "商家拒绝退款"
				return
			case inventory.ProcessOrderStatus_ProcessOrderReturned:
				switch pay.PayOrderStatus(refund.PayStatus) {
				case pay.PayOrderStatus_REFUND_ORDER_STATUS_MARKED_SUCCESS, pay.PayOrderStatus_REFUND_ORDER_STATUS_SUCCESS:
					status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_SUCCESS
					statusDesc = "退款成功"
					return
				case pay.PayOrderStatus_PAY_ORDER_STATUS_FAILED:
					status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_FAILED
					statusDesc = "受支付方式限制无法原路退回，请与商家及时协商线下或线上退款"
					return
				default:
					// 退款提交等状态对C端视为等待揽收
					status = mall.MallCustomerRefundStatus_MALL_CUSTOMER_REFUND_RETURNING
					statusDesc = "退货品已寄出，等待商家签收"
					return
				}
			default:
				panic(fmt.Sprintf("仓库状态不合法: %d", refund.InventoryStatus))
			}
		}
	default:
		panic(fmt.Sprintf("退款单状态不合法: %d", refund.Status))
	}
	return
}

// 订单分类列表
func (s *Service) CustomerOrderQuery(ctx context.Context, req *mall.CustomerOrderQueryRequest) (
	resp *mall.CustomerOrderQueryResp, err error) {
	var (
		orders    []*model.Order
		requests  []*model.OrderRequest
		details   []*model.OrderDetail
		memberIds []int64
	)
	if memberIds, err = s.downstream.QueryMemberIDs(ctx, req.Token.MemberId, req.Token.SgId); err != nil {
		return
	}
	if orders, err = s.dao.FindMemberOrdersByPage(ctx, req.Token.SgId, req.PageNo, req.PageSize, memberIds,
		req.Category, req.Query); err != nil {
		return
	}
	resp = &mall.CustomerOrderQueryResp{Orders: make([]*mall.CustomerSimpleOrderView, len(orders))}
	if len(orders) == 0 {
		return
	}
	requestIdMap := make(util.Int64BoolMap)
	orderIdMap := make(util.Int64BoolMap)
	for _, order := range orders {
		requestIdMap[order.RequestId] = true
		orderIdMap[order.ID] = true
	}
	requestMap := make(map[uint64]*model.OrderRequest)
	if requests, err = s.dao.FindOrderRequestByIds(ctx, requestIdMap.GetKeys()); err != nil {
		return
	}
	for _, request := range requests {
		requestMap[request.ID] = request
	}
	detailMap := make(map[uint64][]*mall.CustomerSimpleDetailView)
	if details, err = s.dao.FindOrderDetailByOrderIds(ctx, orderIdMap.GetKeys()); err != nil {
		return
	}
	for _, detail := range details {
		if _, ok := detailMap[detail.OrderId]; !ok {
			detailMap[detail.OrderId] = make([]*mall.CustomerSimpleDetailView, 0)
		}
		detailMap[detail.OrderId] = append(detailMap[detail.OrderId], &mall.CustomerSimpleDetailView{
			Id:       detail.ID,
			ProdName: detail.ProdName,
			SkuName:  detail.SkuName,
			Quantity: detail.Quantity,
			// 这里返回商品原始总额，不返回折后金额
			Trans: detail.Price * detail.Quantity,
			Pic:   detail.Pic,
		})
	}
	for i, order := range orders {
		inventoryRemark := &model.InventoryRemark{}
		if err = json.Unmarshal(order.InventoryRemark, inventoryRemark); err != nil {
			err = ecode.WithStack(err)
			return
		}
		request := requestMap[order.RequestId]
		resp.Orders[i] = &mall.CustomerSimpleOrderView{
			OrderNumber:   order.OrderNumber,
			RequestId:     request.BizNo,
			SubmitTime:    order.SubmitTime.Format(util.DefaultLayout),
			DeliveryType:  inventory.DeliverType(order.DeliverType),
			WarehouseName: inventoryRemark.WarehouseName,
			PickUpCode:    inventoryRemark.ExtractKey,
			Trans:         order.Trans,
			Details:       detailMap[order.ID],
		}
		resp.Orders[i].Status, resp.Orders[i].StatusDesc = getCustomerStatusAndDesc(
			order, pay.PayOrderStatus(request.PayStatus))
		if request.AutoCancelTime.Valid {
			resp.Orders[i].ExpireTime = request.AutoCancelTime.Time.Format(util.DefaultLayout)
		}
	}
	return
}

//订单详情
func (s *Service) GetCustomerOrderInfo(ctx context.Context, req *mall.CustomerOrderRequest) (
	resp *mall.CustomerOrderInfoResp, err error) {
	var (
		order         *model.Order
		details       []*model.OrderDetail
		discounts     []*model.OrderDiscount
		request       *model.OrderRequest
		refundDetails []*model.RefundDetailModel
	)
	if order, err = s.dao.GetOrderByOrderNumber(ctx, req.OrderNumber); err != nil {
		if errors.Cause(err) == gorm.ErrRecordNotFound {
			err = constant.ErrObjectNotExist
			return
		}
		return
	}
	if err = s.checkMemberId(ctx, req.Token, order.MemberId); err != nil {
		return
	}
	if request, err = s.dao.GetOrderRequestByID(ctx, order.RequestId); err != nil {
		return
	}
	if details, err = s.dao.FindOrderDetailByOrderIds(ctx, []uint64{order.ID}); err != nil {
		return
	}
	if refundDetails, err = s.dao.FindRefundDetailByOrderId(ctx, order.ID); err != nil {
		return
	}
	if order.Discount != 0 {
		if discounts, err = s.dao.FindOrderDiscountsByOrderId(ctx, order.ID); err != nil {
			return
		}
	}
	addressInfo := &mall.AddressInfo{}
	if err = json.Unmarshal(request.AddressInfo, addressInfo); err != nil {
		err = ecode.WithStack(err)
		return
	}
	payRemark := &model.PayRemark{}
	if err = json.Unmarshal(request.PayRemark, payRemark); err != nil {
		err = ecode.WithStack(err)
		return
	}
	inventoryRemark := &model.InventoryRemark{}
	if err = json.Unmarshal(order.InventoryRemark, inventoryRemark); err != nil {
		err = ecode.WithStack(err)
		return
	}
	resp = &mall.CustomerOrderInfoResp{
		OrderNumber:   order.OrderNumber,
		RequestId:     request.BizNo,
		SubmitTime:    order.SubmitTime.Format(util.DefaultLayout),
		DeliveryType:  inventory.DeliverType(order.DeliverType),
		WarehouseId:   order.WarehouseId,
		WarehouseName: inventoryRemark.WarehouseName,
		DeliveryFee:   order.DeliverFee,
		Details:       make([]*mall.CustomerDetailView, len(details)),
		Discounts:     make([]*mall.CustomerDiscountView, len(discounts)),
		AddressInfo:   addressInfo,
		DeliveryInfo: &mall.DeliveryInfo{
			PlatformName: inventoryRemark.Platform,
			PlatformNo:   inventoryRemark.PlatformOrderNum,
		},
		PickUpInfo: &mall.PickUpInfo{
			Address:    inventoryRemark.PickUpAddress,
			Deadline:   inventoryRemark.DeadlineTime,
			PickUpCode: inventoryRemark.ExtractKey,
		},
		Total:       order.ProdTotal,
		Discount:    order.Discount,
		Trans:       order.Trans,
		PayTypeName: payRemark.PayTypeName,
		Remark:      order.Remark,
		Refunded:    order.Refunded,
	}
	resp.Status, resp.StatusDesc = getCustomerStatusAndDesc(order, pay.PayOrderStatus(request.PayStatus))
	if order.BookingDate.Valid {
		resp.BookingValidTime = order.BookingDate.Time.Format(util.DateLayout)
	}
	if order.RefundDeadline.Valid {
		resp.RefundDeadline = order.RefundDeadline.Time.Format(util.DateLayout)
	}
	if order.CompleteTime.Valid {
		resp.SignTime = order.CompleteTime.Time.Format(util.DefaultLayout)
	}
	if order.AutoConfirmTime.Valid {
		resp.DeliveryInfo.AutoConfirmTime = order.AutoConfirmTime.Time.Format(util.DateLayout)
	}
	if request.AutoCancelTime.Valid {
		resp.ExpireTime = request.AutoCancelTime.Time.Format(util.DefaultLayout)
	}
	refundQueryDetailMap := make(map[uint64][]*mall.CustomerSimpleRefundView)
	for _, detail := range refundDetails {
		if _, ok := refundQueryDetailMap[detail.DetailId]; !ok {
			refundQueryDetailMap[detail.DetailId] = make([]*mall.CustomerSimpleRefundView, 0)
		}
		refundStatus, _ := getCustomerRefundStatusAndDesc(&model.Refund{
			Status: detail.Status, PayStatus: detail.PayStatus, InventoryStatus: detail.InventoryStatus,
			ApplyType: detail.ApplyType})
		refundQueryDetailMap[detail.DetailId] = append(refundQueryDetailMap[detail.DetailId],
			&mall.CustomerSimpleRefundView{
				ApplyNumber:     detail.ApplyNumber,
				RefundAmount:    detail.Trans,
				PlatformOrderNo: detail.PlatformOrderNo,
				PlatformName:    detail.PlatformName,
				Status:          refundStatus,
			})
	}
	promotionNotFinished := order.Discount != 0 && !order.PromotionStatus
	for i, detail := range details {
		resp.Details[i] = &mall.CustomerDetailView{
			Id:             detail.ID,
			ProdId:         detail.ProdId,
			ProdName:       detail.ProdName,
			SkuId:          detail.SkuId,
			SkuName:        detail.SkuName,
			Pic:            detail.Pic,
			Quantity:       detail.Quantity,
			Price:          detail.Price,
			RefundQuantity: detail.RefundedQuantity,
		}
		var ok bool
		if resp.Details[i].Refunds, ok = refundQueryDetailMap[detail.ID]; !ok {
			resp.Details[i].Refunds = make([]*mall.CustomerSimpleRefundView, 0)
		}
		if !promotionNotFinished {
			resp.Details[i].CanRefund = 0
		} else {
			resp.Details[i].CanRefund = detail.Trans - detail.RefundedTrans
		}
	}
	for i, discount := range discounts {
		resp.Discounts[i] = &mall.CustomerDiscountView{
			Name:     discount.Name,
			Discount: discount.Discount,
		}
	}
	return
}

//基于请求号查询参与订单摘要
func (s *Service) GetCustomerOrderCombine(ctx context.Context, req *mall.TriggerPayRequest) (
	resp *mall.GetCustomerOrderCombineResp, err error) {
	var (
		request *model.OrderRequest
		orders  []*model.OrderDigestModel
	)
	if request, err = s.dao.GetOrderRequestByBizNo(ctx, req.RequestId); err != nil {
		return
	}
	if err = s.checkMemberId(ctx, req.Token, request.MemberId); err != nil {
		return
	}
	payRemark := &model.PayRemark{}
	if err = json.Unmarshal(request.PayRemark, payRemark); err != nil {
		err = ecode.WithStack(err)
		return
	}
	if orders, err = s.dao.FindOrderDigestsByRequestID(ctx, request.ID); err != nil {
		return
	}
	resp = &mall.GetCustomerOrderCombineResp{
		Count:     uint32(len(orders)),
		Trans:     request.Trans,
		PayMethod: payRemark.PayType,
		Orders:    make([]*mall.CombinePayOrder, len(orders)),
	}
	for i, order := range orders {
		resp.Orders[i] = &mall.CombinePayOrder{
			DeliveryType:  inventory.DeliverType(order.DeliverType),
			WarehouseName: order.WarehouseName,
			Trans:         order.Trans,
			ProdCount:     order.ProdCount,
		}
	}
	return
}

func getSourceType(token *mall.CustomerToken) (sourceType pay.SourceType, err error) {
	if sourceKey, ok := pay.SourceType_value[token.AppType]; !ok {
		err = ecode.BadRequest("当前用户场景值未配置")
		return
	} else {
		sourceType = pay.SourceType(sourceKey)
		return
	}
}

func getPayTypeBySourceType(sourceType pay.SourceType, useBalance bool) (payType pay.PayType) {
	if useBalance {
		payType = pay.PayType_PAY_MEMBER
	} else {
		switch sourceType {
		case pay.SourceType_mall_mina:
			payType = pay.PayType_PAY_WECHAT
		case pay.SourceType_mall_alipay:
			payType = pay.PayType_PAY_ALIPAY
		case pay.SourceType_mall_byte:
			payType = pay.PayType_PAY_BYTEDANCE
		default:
			panic(fmt.Sprintf("渠道支付方式未配置: %d", sourceType))
		}
	}
	return
}

//唤起/重试组合支付
func (s *Service) CreateCombinePay(ctx context.Context, req *mall.CreateCombinePayRequest) (
	resp *pay.DoPayOrderView, err error) {
	var processSchedule *model.Schedule
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var (
			sourceType pay.SourceType
			payRemark  json.RawMessage
		)
		request := &model.OrderRequest{}
		now := time.Now()
		if sourceType, err = getSourceType(req.Token); err != nil {
			return
		}
		if err = s.dao.LockRequest(ctx, request, req.RequestId); err != nil {
			return
		}
		if err = s.checkMemberId(ctx, req.Token, request.MemberId); err != nil {
			return
		}
		switch pay.PayOrderStatus(request.PayStatus) {
		case pay.PayOrderStatus_PAY_ORDER_STATUS_CLOSED:
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "订单已关闭，请重新下单")
			return
		case pay.PayOrderStatus_PAY_ORDER_STATUS_PAYING:
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "订单正在支付中，请耐心等待支付结果")
			return
		case pay.PayOrderStatus_PAY_ORDER_STATUS_SUCCESS:
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "订单已经支付成功，请刷新列表查看")
			return
		case pay.PayOrderStatus_PAY_ORDER_STATUS_FAILED, pay.PayOrderStatus_PAY_ORDER_STATUS_INIT:
			payType := getPayTypeBySourceType(sourceType, req.BalancePay)
			if resp, err = s.downstream.CreatePaymentOrder(
				ctx, request, req.Token.AppId, req.Token.OpenId, request.PStoreId,
				sourceType, payType, request.CardInfo); err != nil {
				return
			}
			switch resp.Status {
			case pay.PayOrderStatus_PAY_ORDER_STATUS_SUCCESS:
				if processSchedule, err = s.updatePayResultInTransaction(ctx, resp.SuccessResult, request); err != nil {
					return
				}
			case pay.PayOrderStatus_PAY_ORDER_STATUS_PAYING:
				// 一步创建到支付中状态，需要创建定时任务
				if err = s.dao.Create(ctx, &model.Schedule{
					ScheduleType: uint8(mall.ScheduleType_SCHEDULE_TYPE_PAYING),
					OrderId:      request.ID,
					ExecuteTime:  now.Add(time.Second * 15),
				}); err != nil {
					return
				}
				fallthrough
			default:
				// 未支付 & 支付中 & 支付失败
				request.PayOrderNumber = resp.OrderNumber
				request.PayStatus = uint8(resp.Status)
				if payRemark, err = json.Marshal(&model.PayRemark{
					PayType:     payType,
					PayTypeName: resp.PayTypeName,
				}); err != nil {
					err = ecode.WithStack(err)
					return
				}
				request.PayRemark = orm.JSON(payRemark)
				if err = s.dao.Save(ctx, request); err != nil {
					return
				}
			}
		default:
			err = ecode.WithStack(constant.ErrSystemError)
			return
		}
		return
	}); err != nil {
		return
	}
	// 支付成功时直接创建履约单
	if processSchedule != nil {
		if err = s.createProcessOrder(ctx, processSchedule.OrderId); err != nil {
			return
		}
	}
	return
}

// 前端感知支付成功调用一次，用于开启查询定时任务；此后可以触发轮询
func (s *Service) CombinedPayTrigger(ctx context.Context, req *mall.TriggerPayRequest) (
	resp *mall.PayStatusResp, err error) {
	var (
		payOrderView                    *pay.PayOrderView
		payingSchedule, processSchedule *model.Schedule
	)
	request := &model.OrderRequest{}
	now := time.Now()
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		if err = s.dao.LockRequest(ctx, request, req.RequestId); err != nil {
			return
		}
		if err = s.checkMemberId(ctx, req.Token, request.MemberId); err != nil {
			return
		}
		payStatus := pay.PayOrderStatus(request.PayStatus)
		if payStatus != pay.PayOrderStatus_PAY_ORDER_STATUS_INIT || request.PayOrderNumber == "" {
			resp = &mall.PayStatusResp{Status: payStatus}
			return
		}
		payingSchedule = &model.Schedule{
			ScheduleType: uint8(mall.ScheduleType_SCHEDULE_TYPE_PAYING),
			OrderId:      request.ID,
			ExecuteTime:  now.Add(time.Second * 5),
		}
		if err = s.dao.Save(ctx, payingSchedule); err != nil {
			return
		}
		request.PayStatus = uint8(pay.PayOrderStatus_PAY_ORDER_STATUS_PAYING)
		if err = s.dao.Save(ctx, request); err != nil {
			return
		}
		return
	}); err != nil {
		return
	}
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		if err = s.dao.LockRequest(ctx, request, req.RequestId); err != nil {
			return
		}
		if request.PayStatus != uint8(pay.PayOrderStatus_PAY_ORDER_STATUS_PAYING) {
			return
		}
		if payOrderView, err = s.downstream.TriggerPayment(ctx, request.PayOrderNumber); err != nil {
			return
		}
		if processSchedule, err = s.updatePayResultInTransaction(ctx, payOrderView, request); err != nil {
			return
		}
		resp = &mall.PayStatusResp{Status: payOrderView.Status}
		return
	}); err != nil {
		return
	}
	if processSchedule != nil {
		if err = s.createProcessOrder(ctx, processSchedule.OrderId); err != nil {
			return
		}
	}
	return
}

// 前端感知支付成功调用一次后，开始的轮询请求，轮询到status=true时可以跳转订单详情，失败时停留在结算页即可
func (s *Service) CombinedPayScheduleQuery(ctx context.Context, req *mall.TriggerPayRequest) (
	resp *mall.PayStatusResp, err error) {
	var request *model.OrderRequest
	if request, err = s.dao.GetOrderRequestByBizNo(ctx, req.RequestId); err != nil {
		return
	}
	if err = s.checkMemberId(ctx, req.Token, request.MemberId); err != nil {
		return
	}
	resp = &mall.PayStatusResp{Status: pay.PayOrderStatus(request.PayStatus)}
	return
}

// 取消待组合支付订单
func (s *Service) CancelCombinePay(ctx context.Context, req *mall.TriggerPayRequest) (
	resp *mall.DefaultResp, err error) {
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		request := &model.OrderRequest{}
		now := time.Time{}
		if err = s.dao.LockRequest(ctx, request, req.RequestId); err != nil {
			return
		}
		if err = s.checkMemberId(ctx, req.Token, request.MemberId); err != nil {
			return
		}
		payStatus := pay.PayOrderStatus(request.PayStatus)
		switch payStatus {
		case pay.PayOrderStatus_PAY_ORDER_STATUS_SUCCESS:
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "订单已支付成功，无法取消支付")
			return
		case pay.PayOrderStatus_PAY_ORDER_STATUS_CLOSED:
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "订单已经取消，无需重复操作")
			return
		case pay.PayOrderStatus_PAY_ORDER_STATUS_INIT, pay.PayOrderStatus_PAY_ORDER_STATUS_PAYING:
			if err = s.downstream.CancelPayment(ctx, request.PayOrderNumber); err != nil {
				return
			}
			// 关闭请求
			if err = s.closeBizRequestInTransaction(ctx, request, now); err != nil {
				return
			}
			// 关闭商城订单
			if err = s.dao.CloseMallOrdersByRequestID(
				ctx, request.ID, uint8(mall.MallOrderStatus_MALL_CLOSED_BEFORE_PAYED)); err != nil {
				return
			}
		}
		return
	}); err != nil {
		return
	}
	resp = &mall.DefaultResp{Status: true}
	return
}

func (s *Service) checkMemberId(ctx context.Context, token *mall.CustomerToken, targetMemberId uint64) (err error) {
	if token.MemberId == targetMemberId {
		return
	}
	var memberIds []int64
	if memberIds, err = s.downstream.QueryMemberIDs(ctx, token.MemberId, token.SgId); err != nil {
		return
	}
	for _, memberId := range memberIds {
		if uint64(memberId) == targetMemberId {
			return
		}
	}
	err = ecode.AppendMessage(constant.ErrForbiddenAction, "当前会员无权访问对应资源")
	return
}

// 订单确认收货
func (s *Service) ConfirmOrderReceived(ctx context.Context, req *mall.CustomerOrderRequest) (
	resp *mall.DefaultResp, err error) {
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		order := &model.Order{}
		if err = s.dao.LockOrder(ctx, order, req.OrderNumber); err != nil {
			return
		}
		if err = s.checkMemberId(ctx, req.Token, order.MemberId); err != nil {
			return
		}
		if err = s.confirmInTransaction(ctx, order); err != nil {
			return
		}
		return
	}); err != nil {
		return
	}
	resp = &mall.DefaultResp{Status: true}
	return
}

func (s *Service) cancelBeforeSending(ctx context.Context, order *model.Order, request *model.OrderRequest,
	details []*model.OrderDetail, req *mall.OrderRefundApplyRequest) (applyNumber string, err error) {
	var (
		refundRemark, warehouseRemark, payRemark json.RawMessage
		warehouseInfo                            *inventory.GetWarehouseAddressInfoReply
	)
	for _, detail := range details {
		if detail.IsRefunding {
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "存在待审核退款申请，不允许重复提交")
			return
		}
	}
	now := time.Now()
	if applyNumber, err = generator.GenerateOrderNumber(
		now, int(mall.OrderType_ORDER_TYPE_REFUND), strconv.Itoa(int(order.MemberId))); err != nil {
		return
	}
	if refundRemark, err = json.Marshal(&model.RefundApplyRemark{
		Memo:   req.Remark,
		Reason: req.Reason,
	}); err != nil {
		err = ecode.WithStack(err)
		return
	}
	if warehouseInfo, err = s.downstream.GetWarehouseInfo(
		ctx, order.SgId, order.PStoreId, order.WarehouseId); err != nil {
		return
	}
	if warehouseRemark, err = json.Marshal(&model.WarehouseRemark{
		AddressInfo: *warehouseInfo.AddressInfo,
		Name:        warehouseInfo.Name,
	}); err != nil {
		return
	}
	requestPayRemark := &model.PayRemark{}
	if err = json.Unmarshal(request.PayRemark, requestPayRemark); err != nil {
		return
	}
	if payRemark, err = json.Marshal(&model.PayRemark{
		PayType:     requestPayRemark.PayType,
		PayTypeName: requestPayRemark.PayTypeName,
	}); err != nil {
		err = ecode.WithStack(err)
		return
	}
	apply := &model.Refund{
		ApplyNumber:   applyNumber,
		OrderId:       order.ID,
		ApplyType:     uint8(req.Type),
		Status:        uint8(mall.MallRefundStatus_MALL_REFUND_INIT),
		SgId:          order.SgId,
		PStoreId:      order.PStoreId,
		MemberId:      order.MemberId,
		ApplyTime:     now,
		Remark:        orm.JSON(refundRemark),
		WarehouseId:   order.WarehouseId,
		WarehouseInfo: orm.JSON(warehouseRemark),
		Trans:         order.Trans,
		ReviewTime:    sql.NullTime{},
		ReviewRemark:  constant.EmptyBytes,
		PayStatus:     uint8(pay.PayOrderStatus_REFUND_ORDER_STATUS_INIT),
		PayRemark:     orm.JSON(payRemark),
		DeliverType:   uint8(inventory.DeliverType_SKIP),
	}
	if err = s.dao.Create(ctx, apply); err != nil {
		return
	}
	for _, detail := range details {
		if err = s.dao.Create(ctx, &model.RefundDetail{
			ApplyId:  apply.ID,
			MemberId: order.MemberId,
			OrderId:  order.ID,
			DetailId: detail.ID,
			ProdId:   detail.ProdId,
			SkuId:    detail.SkuId,
			ProdName: detail.ProdName,
			SkuName:  detail.SkuName,
			Pic:      detail.Pic,
			Unit:     detail.Unit,
			Price:    detail.Price,
			Quantity: detail.Quantity,
			Trans:    detail.Trans,
		}); err != nil {
			return
		}
	}
	if err = s.dao.UpdateDetailIsRefundingByOrderId(ctx, order.ID, true); err != nil {
		return
	}
	var notifyMsg json.RawMessage
	if notifyMsg, err = json.Marshal(&inventory.InventoryOrderNotifyPayload{
		OrderNumber: order.OrderNumber,
		Action:      "refund_apply",
		Timestamp:   now.Unix(),
		Source:      inventory.SourceEnum_MALL_ORDER,
	}); err != nil {
		return
	}
	if err = s.dao.PublishUniqueNSQMessage(ctx, constant.TopicInventoryNotify, notifyMsg); err != nil {
		return
	}
	return
}

func (s *Service) cancelAfterSending(ctx context.Context, order *model.Order, request *model.OrderRequest,
	details []*model.OrderDetail, req *mall.OrderRefundApplyRequest) (applyNumber string, err error) {
	var (
		refundRemark, warehouseRemark, payRemark json.RawMessage
		warehouseInfo                            *inventory.GetWarehouseAddressInfoReply
		refundTrans                              uint64
		deliverType                              inventory.DeliverType
	)
	detailMap := make(map[uint64]*model.OrderDetail)
	for _, detail := range details {
		detailMap[detail.ID] = detail
	}
	if len(req.Details) == 0 {
		err = ecode.BadRequest("必须传入选择明细")
		return
	}
	// 需要验证可退金额 & 可退数量
	for _, refundDetail := range req.Details {
		if detail, ok := detailMap[refundDetail.Id]; !ok {
			err = ecode.AppendMessage(constant.ErrNeedRefresh, "当前订单已更新，请刷新列表重试")
			return
		} else if detail.IsRefunding {
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "所选明细存在待审核售后申请，不允许继续发起退款")
			return
		} else if detail.Trans-detail.RefundedTrans-refundDetail.Amount < 0 {
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "所选明细没有可退金额")
			return
		} else if refundDetail.Quantity > 0 {
			if req.Type == mall.MallRefundType_MALL_REFUND_ONLY {
				err = ecode.BadRequest("仅退款时，不能选择明细数量")
				return
			}
			if detail.Quantity-detail.RefundedQuantity-refundDetail.Quantity < 0 {
				err = ecode.AppendMessage(constant.ErrForbiddenAction, "所选明细没有可退数量")
				return
			}
		} else if req.Type == mall.MallRefundType_MALL_REFUND_WITH_DETAIL {
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "退货退款模式明细数量不能为0")
			return
		}
		refundTrans += refundDetail.Amount
	}
	now := time.Now()
	if applyNumber, err = generator.GenerateOrderNumber(
		now, int(mall.OrderType_ORDER_TYPE_REFUND), strconv.Itoa(int(order.MemberId))); err != nil {
		return
	}
	if refundRemark, err = json.Marshal(&model.RefundApplyRemark{
		Memo: req.Remark,
	}); err != nil {
		err = ecode.WithStack(err)
		return
	}
	if warehouseInfo, err = s.downstream.GetWarehouseInfo(
		ctx, order.SgId, order.PStoreId, order.WarehouseId); err != nil {
		return
	}
	if warehouseRemark, err = json.Marshal(&model.WarehouseRemark{
		AddressInfo: *warehouseInfo.AddressInfo,
		Name:        warehouseInfo.Name,
	}); err != nil {
		return
	}
	requestPayRemark := &model.PayRemark{}
	if err = json.Unmarshal(request.PayRemark, requestPayRemark); err != nil {
		return
	}
	if payRemark, err = json.Marshal(&model.PayRemark{
		PayType:     requestPayRemark.PayType,
		PayTypeName: requestPayRemark.PayTypeName,
	}); err != nil {
		err = ecode.WithStack(err)
		return
	}
	switch req.Type {
	case mall.MallRefundType_MALL_REFUND_ONLY:
		deliverType = inventory.DeliverType_SKIP
	case mall.MallRefundType_MALL_REFUND_WITH_DETAIL:
		deliverType = inventory.DeliverType_ORDINARY
	default:
		panic(fmt.Sprintf("退款申请类型没有可用分支: %d", req.Type))
	}
	apply := &model.Refund{
		ApplyNumber:   applyNumber,
		OrderId:       order.ID,
		ApplyType:     uint8(req.Type),
		Status:        uint8(mall.MallRefundStatus_MALL_REFUND_INIT),
		SgId:          order.SgId,
		PStoreId:      order.PStoreId,
		MemberId:      order.MemberId,
		ApplyTime:     now,
		Remark:        orm.JSON(refundRemark),
		WarehouseId:   order.WarehouseId,
		WarehouseInfo: orm.JSON(warehouseRemark),
		Trans:         refundTrans,
		ReviewTime:    sql.NullTime{},
		ReviewRemark:  constant.EmptyBytes,
		PayStatus:     uint8(pay.PayOrderStatus_REFUND_ORDER_STATUS_INIT),
		PayRemark:     orm.JSON(payRemark),
		DeliverType:   uint8(deliverType),
	}
	if err = s.dao.Create(ctx, apply); err != nil {
		return
	}
	detailIds := make([]uint64, len(req.Details))
	for i, refundDetail := range req.Details {
		detail := detailMap[refundDetail.Id]
		detailIds[i] = refundDetail.Id
		if err = s.dao.Create(ctx, &model.RefundDetail{
			ApplyId:  apply.ID,
			MemberId: order.MemberId,
			OrderId:  order.ID,
			DetailId: detail.ID,
			Reason:   refundDetail.Reason,
			ProdId:   detail.ProdId,
			SkuId:    detail.SkuId,
			ProdName: detail.ProdName,
			SkuName:  detail.SkuName,
			Pic:      detail.Pic,
			Unit:     detail.Unit,
			Price:    detail.Price,
			Quantity: refundDetail.Quantity,
			Trans:    refundDetail.Amount,
		}); err != nil {
			return
		}
	}
	if err = s.dao.UpdateDetailIsRefundingByDetailIds(ctx, detailIds, true); err != nil {
		return
	}
	var notifyMsg json.RawMessage
	if notifyMsg, err = json.Marshal(&inventory.InventoryOrderNotifyPayload{
		OrderNumber: order.OrderNumber,
		Action:      "refund_apply",
		Timestamp:   now.Unix(),
		Source:      inventory.SourceEnum_MALL_ORDER,
	}); err != nil {
		return
	}
	if err = s.dao.PublishUniqueNSQMessage(ctx, constant.TopicInventoryNotify, notifyMsg); err != nil {
		return
	}
	return
}

// 订单申请售后
func (s *Service) SubmitOrderRefundApply(ctx context.Context, req *mall.OrderRefundApplyRequest) (
	resp *mall.OrderRefundApplyResp, err error) {
	resp = &mall.OrderRefundApplyResp{}
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var (
			request *model.OrderRequest
			details []*model.OrderDetail
		)
		order := &model.Order{}
		if err = s.dao.LockOrder(ctx, order, req.OrderNumber); err != nil {
			return
		}
		if err = s.checkMemberId(ctx, req.Token, order.MemberId); err != nil {
			return
		}
		if mall.MallOrderStatus(order.Status) != mall.MallOrderStatus_MALL_PAYED {
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "只有已支付订单才能申请售后")
			return
		}
		if request, err = s.dao.GetOrderRequestByID(ctx, order.RequestId); err != nil {
			return
		}
		if details, err = s.dao.FindOrderDetailByOrderIds(ctx, []uint64{order.ID}); err != nil {
			return
		}
		switch inventory.ProcessOrderStatus(order.InventoryStatus) {
		case inventory.ProcessOrderStatus_ProcessOrderCancelled:
			err = ecode.AppendMessage(constant.ErrNeedRefresh, "订单已取消")
			return
		case inventory.ProcessOrderStatus_ProcessOrderWaitStock, inventory.ProcessOrderStatus_ProcessOrderWaitShip:
			// 仅允许申请整单取消
			if req.Type != mall.MallRefundType_MALL_REFUND_CANCEL_BEFORE_SENDING {
				err = ecode.AppendMessage(constant.ErrNeedRefresh, "仅允许发起整单取消")
				return
			}
			if resp.ApplyNumber, err = s.cancelBeforeSending(
				ctx, order, request, details, req); err != nil {
				return
			}
		case inventory.ProcessOrderStatus_ProcessOrderWaitMentioned,
			inventory.ProcessOrderStatus_ProcessOrderShipped,
			inventory.ProcessOrderStatus_ProcessOrderReceived:
			// 仅允许 仅退款 & 退货退款
			if req.Type == mall.MallRefundType_MALL_REFUND_CANCEL_BEFORE_SENDING {
				err = ecode.AppendMessage(constant.ErrNeedRefresh, "仅允许发起退款 & 退货退款")
				return
			}
			if order.Discount != 0 && !order.PromotionStatus {
				err = ecode.AppendMessage(constant.ErrForbiddenAction, "订单尚未分摊完毕，请稍后再试")
				return
			}
			if resp.ApplyNumber, err = s.cancelAfterSending(
				ctx, order, request, details, req); err != nil {
				return
			}
		default:
			panic(fmt.Sprintf("仓库状态未对接售后流程分支: %d", order.InventoryStatus))
		}
		return
	}); err != nil {
		return
	}
	return
}

//申请售后申请列表
func (s *Service) CustomerQueryOrderRefundApplies(ctx context.Context, req *mall.CustomerQueryOrderRefundAppliesRequest) (
	resp *mall.CustomerQueryOrderRefundAppliesResp, err error) {
	var (
		applies      []*model.Refund
		orders       []*model.Order
		memberIds    []int64
		applyDetails []*model.RefundDetail
	)
	if memberIds, err = s.downstream.QueryMemberIDs(ctx, req.Token.MemberId, req.Token.SgId); err != nil {
		return
	}
	if applies, err = s.dao.FindMemberRefundsByPage(
		ctx, req.Token.SgId, req.PageNo, req.PageSize, memberIds); err != nil {
		return
	}
	resp = &mall.CustomerQueryOrderRefundAppliesResp{
		Applies: make([]*mall.OrderRefundApplySimpleView, len(applies))}
	if len(applies) == 0 {
		return
	}
	applyIds := make([]uint64, len(applies))
	orderIdMap := make(util.Int64BoolMap)
	orderNumberMap := make(map[uint64]string)
	detailMap := make(map[uint64][]*model.RefundDetail)
	for i, apply := range applies {
		applyIds[i] = apply.ID
		orderIdMap[apply.OrderId] = true
	}
	if applyDetails, err = s.dao.FindRefundDetailsByApplyIds(ctx, applyIds); err != nil {
		return
	}
	if orders, err = s.dao.FindOrdersByOrderIds(ctx, orderIdMap.GetKeys()); err != nil {
		return
	}
	for _, order := range orders {
		orderNumberMap[order.ID] = order.OrderNumber
	}
	for _, detail := range applyDetails {
		if _, ok := detailMap[detail.ApplyId]; !ok {
			detailMap[detail.ApplyId] = make([]*model.RefundDetail, 0)
		}
		detailMap[detail.ID] = append(detailMap[detail.ID], detail)
	}
	for i, apply := range applies {
		warehouseRemark := &model.WarehouseRemark{}
		if err = json.Unmarshal(apply.WarehouseInfo, warehouseRemark); err != nil {
			err = ecode.WithStack(err)
			return
		}
		refApplyDetails := detailMap[apply.ID]
		resp.Applies[i] = &mall.OrderRefundApplySimpleView{
			ApplyNumber:   apply.ApplyNumber,
			Type:          mall.MallRefundType(apply.ApplyType),
			OrderNumber:   orderNumberMap[apply.OrderId],
			ApplyTime:     apply.ApplyTime.Format(util.DefaultLayout),
			WarehouseName: warehouseRemark.Name,
			Trans:         apply.Trans,
			Details:       make([]*mall.RefundApplyDetailView, len(refApplyDetails)),
			DeliverType:   inventory.DeliverType(apply.DeliverType),
		}
		resp.Applies[i].Status, resp.Applies[i].StatusDesc = getCustomerRefundStatusAndDesc(apply)
		for j, detail := range refApplyDetails {
			resp.Applies[i].Details[j] = &mall.RefundApplyDetailView{
				Id:       detail.ID,
				Quantity: detail.Quantity,
				Trans:    detail.Trans,
				Reason:   detail.Reason,
				ProdId:   detail.ProdId,
				ProdName: detail.ProdName,
				SkuId:    detail.SkuId,
				SkuName:  detail.SkuName,
				Pic:      detail.Pic,
			}
		}
	}
	return
}

//申请售后申请详情
func (s *Service) CustomerGetOrderRefundApply(ctx context.Context, req *mall.CustomerOrderRefundApplyInfoRequest) (
	resp *mall.OrderRefundApplyView, err error) {
	var (
		apply        *model.Refund
		order        *model.Order
		applyDetails []*model.RefundDetail
	)
	if apply, err = s.dao.GetApplyByApplyNumber(ctx, req.ApplyNumber); err != nil {
		if errors.Cause(err) == gorm.ErrRecordNotFound {
			err = constant.ErrObjectNotExist
			return
		}
		return
	}
	if err = s.checkMemberId(ctx, req.Token, apply.MemberId); err != nil {
		return
	}
	if applyDetails, err = s.dao.FindRefundDetailsByApplyIds(ctx, []uint64{apply.ID}); err != nil {
		return
	}
	if order, err = s.dao.GetOrderByOrderID(ctx, apply.OrderId); err != nil {
		return
	}
	warehouseRemark := &model.WarehouseRemark{}
	if err = json.Unmarshal(apply.WarehouseInfo, warehouseRemark); err != nil {
		err = ecode.WithStack(err)
		return
	}
	applyRemark := &model.RefundApplyRemark{}
	if err = json.Unmarshal(apply.Remark, applyRemark); err != nil {
		err = ecode.WithStack(err)
		return
	}
	resp = &mall.OrderRefundApplyView{
		ApplyNumber:   apply.ApplyNumber,
		Type:          mall.MallRefundType(apply.ApplyType),
		OrderNumber:   order.OrderNumber,
		ApplyTime:     apply.ApplyTime.Format(util.DefaultLayout),
		WarehouseName: warehouseRemark.Name,
		Trans:         apply.Trans,
		Details:       make([]*mall.RefundApplyDetailView, len(applyDetails)),
		Remark:        applyRemark.Memo,
		Reason:        applyRemark.Reason,
		Address:       warehouseRemark.Address,
		Contact:       warehouseRemark.Contact,
		Mobile:        warehouseRemark.Mobile,
		PlatformName:  warehouseRemark.Platform,
		PlatformNo:    warehouseRemark.PlatformOrderNum,
	}
	resp.Status, resp.StatusDesc = getCustomerRefundStatusAndDesc(apply)
	for i, detail := range applyDetails {
		resp.Details[i] = &mall.RefundApplyDetailView{
			Id:       detail.ID,
			Quantity: detail.Quantity,
			Trans:    detail.Trans,
			Reason:   detail.Reason,
			ProdId:   detail.ProdId,
			ProdName: detail.ProdName,
			SkuId:    detail.SkuId,
			SkuName:  detail.SkuName,
			Pic:      detail.Pic,
		}
	}
	return
}

//退货快递单信息提交
func (s *Service) CustomerRefundDeliverySubmit(ctx context.Context, req *mall.CustomerRefundDeliverySubmitRequest) (
	resp *mall.DefaultResp, err error) {
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var (
			order               *model.Order
			orderDetails        []*model.OrderDetail
			applyDetails        []*model.RefundDetail
			warehouseRemarkJson json.RawMessage
		)
		apply := &model.Refund{}
		if err = s.dao.LockApply(ctx, apply, req.ApplyNumber); err != nil {
			return
		}
		if err = s.checkMemberId(ctx, req.Token, apply.MemberId); err != nil {
			return
		}
		if mall.MallRefundStatus(apply.Status) != mall.MallRefundStatus_MALL_REFUND_TO_BE_DELIVER {
			err = ecode.AppendMessage(constant.ErrNeedRefresh, "售后单已更新，请刷新列表重试")
			return
		}
		if applyDetails, err = s.dao.FindRefundDetailsByApplyIds(ctx, []uint64{apply.ID}); err != nil {
			return
		}
		applyRemark := &model.RefundApplyRemark{}
		if err = json.Unmarshal(apply.Remark, applyRemark); err != nil {
			err = ecode.WithStack(err)
			return
		}
		warehouseRemark := &model.WarehouseRemark{}
		if err = json.Unmarshal(apply.WarehouseInfo, warehouseRemark); err != nil {
			err = ecode.WithStack(err)
			return
		}
		warehouseRemark.PlatformOrderNum = req.PlatformNo
		warehouseRemark.Platform = req.PlatformName
		// 创建仓库退货单
		if order, err = s.dao.GetOrderByOrderID(ctx, apply.OrderId); err != nil {
			return
		}
		if orderDetails, err = s.dao.FindOrderDetailByOrderIds(ctx, []uint64{order.ID}); err != nil {
			return
		}
		detailMap := make(map[uint64]*model.OrderDetail)
		for _, detail := range orderDetails {
			detailMap[detail.ID] = detail
		}
		operationDetails := make([]*inventory.OperationDetail, len(applyDetails))
		for i, applyDetail := range applyDetails {
			orderDetail := detailMap[applyDetail.DetailId]
			operationDetails[i] = &inventory.OperationDetail{
				StorageKey: fmt.Sprintf("%d-%d", applyDetail.ProdId, applyDetail.SkuId),
				Quantity:   applyDetail.Quantity,
				Price:      orderDetail.Price,
				Origin:     applyDetail.Trans,
				Discount:   0,
				Trans:      applyDetail.Trans,
				Remark: &inventory.OperationDetailRemark{
					Name:    applyDetail.ProdName,
					Unit:    applyDetail.Unit,
					Reason:  applyDetail.Reason,
					SkuName: applyDetail.SkuName,
				},
			}
		}
		if warehouseRemark.InventoryOrderNum, err = s.downstream.CreateRefundProcessOrder(
			ctx, apply, req, applyRemark, operationDetails); err != nil {
			return
		}
		if warehouseRemarkJson, err = json.Marshal(warehouseRemark); err != nil {
			return
		}
		apply.WarehouseInfo = orm.JSON(warehouseRemarkJson)
		apply.Status = uint8(mall.MallRefundStatus_MALL_REFUND_RETURNING)
		if err = s.dao.Save(ctx, apply); err != nil {
			return
		}
		return
	}); err != nil {
		return
	}
	resp = &mall.DefaultResp{Status: true}
	return
}

func (s *Service) checkRefunding(ctx context.Context, apply *model.Refund,
	order *model.Order, now time.Time) (err error) {
	var (
		isRefunding  bool
		orderDetails []*model.OrderDetail
	)
	if mall.MallRefundType(apply.ApplyType) != mall.MallRefundType_MALL_REFUND_CANCEL_BEFORE_SENDING {
		// 非整单取消时，需要检测明细是否存在仍为退款中的
		if orderDetails, err = s.dao.FindOrderDetailByOrderIds(ctx, []uint64{order.ID}); err != nil {
			return
		}
		for _, detail := range orderDetails {
			if detail.IsRefunding {
				isRefunding = true
			}
		}
	}
	if !isRefunding {
		var notifyMsg json.RawMessage
		if notifyMsg, err = json.Marshal(&inventory.InventoryOrderNotifyPayload{
			OrderNumber: order.OrderNumber,
			Action:      "refund_cancel",
			Timestamp:   now.Unix(),
			Source:      inventory.SourceEnum_MALL_ORDER,
		}); err != nil {
			return
		}
		if err = s.dao.PublishUniqueNSQMessage(ctx, constant.TopicInventoryNotify, notifyMsg); err != nil {
			return
		}
	}
	return
}

//取消退货申请单
func (s *Service) CustomerRefundCancel(ctx context.Context, req *mall.CustomerOrderRefundApplyInfoRequest) (
	resp *mall.DefaultResp, err error) {
	if err = s.dao.Transaction(ctx, func(ctx context.Context) (err error) {
		var (
			order         *model.Order
			orderDetails  []*model.OrderDetail
			refundDetails []*model.RefundDetail
		)
		now := time.Now()
		apply := &model.Refund{}
		if err = s.dao.LockApply(ctx, apply, req.ApplyNumber); err != nil {
			return
		}
		if err = s.checkMemberId(ctx, req.Token, apply.MemberId); err != nil {
			return
		}

		switch mall.MallRefundStatus(apply.Status) {
		case mall.MallRefundStatus_MALL_REFUND_INIT:
			// 需要检查关闭审核中flag标识
			if order, err = s.dao.GetOrderByOrderID(ctx, apply.OrderId); err != nil {
				return
			}
			if refundDetails, err = s.dao.FindRefundDetailsByApplyIds(ctx, []uint64{apply.ID}); err != nil {
				return
			}
			refundDetailIdMap := make(util.Int64BoolMap)
			for _, refundDetail := range refundDetails {
				refundDetailIdMap[refundDetail.DetailId] = true
			}
			if err = s.dao.UpdateDetailIsRefundingByDetailIds(
				ctx, refundDetailIdMap.GetKeys(), false); err != nil {
				return
			}
			if err = s.checkRefunding(ctx, apply, order, now); err != nil {
				return
			}
		case mall.MallRefundStatus_MALL_REFUND_TO_BE_DELIVER:
			// 需要还原订单 & 明细可退金额 & 数量
			if order, err = s.dao.GetOrderByOrderID(ctx, apply.OrderId); err != nil {
				return
			}
			if refundDetails, err = s.dao.FindRefundDetailsByApplyIds(ctx, []uint64{apply.ID}); err != nil {
				return
			}
			refundDetailMap := make(map[uint64]*model.RefundDetail)
			for _, refundDetail := range refundDetails {
				refundDetailMap[refundDetail.DetailId] = refundDetail
			}
			if orderDetails, err = s.dao.FindOrderDetailByOrderIds(ctx, []uint64{order.ID}); err != nil {
				return
			}
			order.Refunded -= apply.Trans
			if err = s.dao.Save(ctx, order); err != nil {
				return
			}
			for _, detail := range orderDetails {
				if refundDetail, ok := refundDetailMap[detail.ID]; ok {
					detail.RefundedQuantity -= refundDetail.Quantity
					detail.RefundedTrans -= refundDetail.Trans
					if err = s.dao.Save(ctx, detail); err != nil {
						return
					}
				}
			}
		default:
			err = ecode.AppendMessage(constant.ErrForbiddenAction, "当前售后单状态禁止取消")
			return
		}
		// 更新售后状态
		apply.Status = uint8(mall.MallRefundStatus_MALL_REFUND_CLOSED)
		if err = s.dao.Save(ctx, apply); err != nil {
			return
		}
		return
	}); err != nil {
		return
	}
	resp = &mall.DefaultResp{Status: true}
	return
}
